package pl.polsl.pum2.pj.rubik;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

import javax.microedition.khronos.opengles.GL10;

import pl.polsl.pum2.pj.math.Quaternion3d;
import pl.polsl.pum2.pj.math.Vector3d;

import android.opengl.Matrix;
import android.util.Log;

public class Rubik {

  private Cube             cubes[];
  private int              size;
  private Rotation         currentRotation;
  private RubiksRandomizer randomizer = new RubiksRandomizer();

  /** space between cubes. minimum = 1f */
  private final float      SPACE      = 1.01f;
  private final float      delta      = 0.2f;

  /**
   * @param size
   *          [2..7] the cubes dimensions is size * size * size.
   * @param randomizeIterations
   *          do thrice as moves to shuffle the cube initially.
   */
  public Rubik(int size, int randomizeIterations) {
    this.size = size;
    int innersize = size - 2;
    cubes = new Cube[size * size * size - innersize * innersize * innersize];
    float offset = -(((float) size) / 2 - 0.5f) * SPACE; // the middle of rubik's cube is located at 0,0,0;

    int i = 0;
    int id = 0;
    for (int x = 0; x < size; ++x) {
      for (int y = 0; y < size; ++y) {
        for (int z = 0; z < size; ++z) {
          if (x == 0 || x == size - 1 || y == 0 || y == size - 1 || z == 0 || z == size - 1) {
            cubes[i++] = new Cube(new Vector3d(offset + x * SPACE, offset + y * SPACE, offset + z * SPACE), id++, x, y, z, size);
          }
        }
      }
    }
    randomizer.randomize(randomizeIterations);
  }

  private boolean aboutEquals(float f1, float f2) {
    return Math.abs(f1 - f2) < delta;
  }

  private boolean aboutEquals(Vector3d v1, Vector3d v2) {
    return aboutEquals(v1.x, v2.x) && aboutEquals(v1.y, v2.y) && aboutEquals(v1.z, v2.z);
  }

  public void draw(GL10 gl) {
    for (Cube cube : cubes) {
      cube.draw(gl);
    }
  }

  /**
   * Updates animations;<br>
   * Returns true if the cube was solved.
   */
  public boolean update(long millis) {
    if (currentRotation != null) {
      if (currentRotation.update(millis)) {
        currentRotation = null;
        return isSolved();
      }
    }
    return false;
  }

  /**
   * Checks if a rotation can be done, if so fires suitable methods depending on the case.
   * 
   * @param fromAlpha
   *          color and ID of source cube
   * @param toAlpha
   *          color and ID of destination cube
   */
  public void rotate(ColorAndId fromAlpha, ColorAndId toAlpha) {

    if (this.currentRotation != null) {
      Log.d("Don't even try that. This cube is unbreakable.", ":)");
      return;
    }
    
    if (fromAlpha.equals(ColorAndId.zero)) {
      return; // no cube selected. background clicked.
    }

    Cube from = find(fromAlpha.getId());
    if (from == null) {
      return; // should not happen..
    }

    if (fromAlpha.getId() == toAlpha.getId()) {
      // source and destination is still the same cube
      if (fromAlpha.equals(toAlpha)) {
        return; // still the same color, nothing to rotate
      } else {
        // the same cube, different side. rotate.
        rotateThemCubes(fromAlpha, toAlpha, from);
      }

    } else {
      // source and destination are not the same cubes
      if (toAlpha.equals(ColorAndId.zero)) {
        return; // dragging off directly to background is not allowed.
      }
      Cube to = find(toAlpha.getId());
      if (to == null) {
        return; // should not happen..
      } else {
        // two different cubes
        Vector3d f = from.getLocationVector();
        Vector3d t = to.getLocationVector();

        // make sure these two cubes are neighbours
        if (Vector3d.distance(f, t) < SPACE + delta) {
          rotateThemCubes(fromAlpha, toAlpha, from, to);
        }
      }

    }
  }

  private Cube find(int alphaId) {
    for (Cube c : cubes) {
      if (c.getId() == alphaId) {
        return c;
      }
    }
    return null;
  }

  private void rotateThemCubes(ColorAndId fromAlpha, ColorAndId toAlpha, Cube fromCube, Cube toCube) {

    float[] constVect = Cube.colorToInitialDirection4(fromAlpha);
    if (constVect == null) {
      return;
    }

    Vector3d from = fromCube.getLocationVector();
    Vector3d to = toCube.getLocationVector();
    Vector3d directionVector = new Vector3d(to.x - from.x, to.y - from.y, to.z - from.z);

    float[] result = new float[4];
    Matrix.multiplyMV(result, 0, fromCube.rotMatrix, 0, constVect, 0);
    Vector3d resultVector = new Vector3d(result[0], result[1], result[2]);
    Vector3d axis = Vector3d.crossProduct(directionVector, resultVector);
    roundToExactAxis(axis);
    
    // select layer
    Cube[] layer = selectLayer(from, axis);

    // finally apply the rotation
    currentRotation = new Rotation(layer, Quaternion3d.fromAxisAndAngle(axis, (float) Math.toRadians(90)));
  }

  private void rotateThemCubes(ColorAndId fromAlpha, ColorAndId toAlpha, Cube theCube) {

    float[] constVect;
    float[] result = new float[4];

    constVect = Cube.colorToInitialDirection4(fromAlpha);
    if (constVect == null) {
      return;
    }
    Matrix.multiplyMV(result, 0, theCube.rotMatrix, 0, constVect, 0);
    Vector3d resultVector1 = new Vector3d(result[0], result[1], result[2]);

    constVect = Cube.colorToInitialDirection4(toAlpha);
    if (constVect == null) {
      return;
    }
    Matrix.multiplyMV(result, 0, theCube.rotMatrix, 0, constVect, 0);
    Vector3d resultVector2 = new Vector3d(result[0], result[1], result[2]);

    Vector3d axis = Vector3d.crossProduct(resultVector2, resultVector1);
    roundToExactAxis(axis);
    
    // select layer
    Cube[] layer = selectLayer(theCube.getLocationVector(), axis);

    // finally apply the rotation
    currentRotation = new Rotation(layer, Quaternion3d.fromAxisAndAngle(axis, (float) Math.toRadians(90)));
  }

  private Cube[] selectLayer(Vector3d location, Vector3d axis) {
    List<Cube> list = new ArrayList<Cube>(size * size);

    if (!aboutEquals(axis.x, 0)) {
      for (Cube cube : cubes) {
        if (aboutEquals(location.x, cube.getLocationVector().x)) {
          list.add(cube);
        }
      }
    } else if (!aboutEquals(axis.y, 0)) {
      for (Cube cube : cubes) {
        if (aboutEquals(location.y, cube.getLocationVector().y)) {
          list.add(cube);
        }
      }
    } else if (!aboutEquals(axis.z, 0)) {
      for (Cube cube : cubes) {
        if (aboutEquals(location.z, cube.getLocationVector().z)) {
          list.add(cube);
        }
      }
    }

    Cube[] layer = new Cube[list.size()];
    return list.toArray(layer);
  }

  public boolean isSolved() {

    boolean sameColor = false;
    Vector3d middleColor;

    if (size > 2) {
      for (RubiksSide side : RubiksSide.values()) {
        Cube[] layer = getExternalLayer(side);
        middleColor = layer[size + 1].getVecColors().get(0);
        for (Cube cube : layer) {
          sameColor = false;

          for (Vector3d color : cube.getVecColors()) {
            if (aboutEquals(color, middleColor)) {
              sameColor = true;
            }
          }

          if (sameColor == false) {
            return false;
          }
        }
        Log.d(side.name(), "OK");
      }
    } else {
      ArrayList<Vector3d> allVectors = new ArrayList<Vector3d>();
      int counter = 0;

      for (RubiksSide side : RubiksSide.values()) {
        allVectors.clear();
        counter = 0;
        Cube[] layer = getExternalLayer(side);

        for (Cube cube : layer) {
          allVectors.addAll(cube.getVecColors());
        }
        // checking if there are 4 same colors on one layer (comparing with 3 colors of first 'cubic')
        for (int i = 0; i < 3; ++i) {
          for (Vector3d color : allVectors) {
            if (aboutEquals(allVectors.get(i), color)) {
              ++counter;
            }
          }
          // four vectors(colors) in the same direction (this is only 2x2x2 rubik) 	
          if (counter == 4) {
            Log.d(side.name(), "OK");
            break;
          }
          counter = 0;
        }
        if (counter == 4) {
          continue;
        }
        return false;
      }
    }

    return true;
  }

  private Cube[] getExternalLayer(RubiksSide side) {
    Cube[] layer = new Cube[size * size];
    int i = 0;

    Vector3d first = cubes[0].getLocationVector(); // yellow/green/orange
    Vector3d last = cubes[size * size - 1].getLocationVector(); // white/blue/red

    switch (side) {
      case TOP: // white
        for (Cube cube : cubes) {
          if (aboutEquals(cube.getLocationVector().y, last.y)) {
            layer[i++] = cube;
          }
        }
        break;
      case BOTTOM: // yellow
        for (Cube cube : cubes) {
          if (aboutEquals(cube.getLocationVector().y, first.y)) {
            layer[i++] = cube;
          }
        }
        break;
      case FRONT: // red
        for (Cube cube : cubes) {
          if (aboutEquals(cube.getLocationVector().z, last.z)) {
            layer[i++] = cube;
          }
        }
        break;
      case BACK: // orange
        for (Cube cube : cubes) {
          if (aboutEquals(cube.getLocationVector().z, first.z)) {
            layer[i++] = cube;
          }
        }
        break;
      case LEFT: // green
        for (Cube cube : cubes) {
          if (aboutEquals(cube.getLocationVector().x, first.x)) {
            layer[i++] = cube;
          }
        }
        break;
      case RIGHT: // blue
        for (Cube cube : cubes) {
          if (aboutEquals(cube.getLocationVector().x, last.x)) {
            layer[i++] = cube;
          }
        }
        break;
    }
    return layer;
  }
  
  private void roundToExactAxis(Vector3d vector)
  {
  	vector.x = Math.round(vector.x);
  	vector.y = Math.round(vector.y);
  	vector.z = Math.round(vector.z);
  }
  
  private enum RubiksSide {
    TOP, BOTTOM, FRONT, BACK, LEFT, RIGHT;
  }

  /**
   * Class for initial shuffling cubicles in a semi-random way.
   */
  private class RubiksRandomizer {

    Random   random = new Random();

    Vector3d X      = new Vector3d(1, 0, 0);
    Vector3d Y      = new Vector3d(0, 1, 0);
    Vector3d Z      = new Vector3d(0, 0, 1);

    double[] angles = {-90, 180, 90};

    public void randomize(int iterations) {
      for (int i = 0; i < iterations; ++i) {
        quickRotate(getRandomXLayer(), X, angles[random.nextInt(3)]);
        quickRotate(getRandomYLayer(), Y, angles[random.nextInt(3)]);
        quickRotate(getRandomZLayer(), Z, angles[random.nextInt(3)]);
      }
    }

    private Cube[] getRandomXLayer() {
      List<Cube> list = new ArrayList<Cube>(size * size);
      float randomXPos = cubes[0].getReferenceVec().x + random.nextInt(size);

      for (Cube cube : cubes) {
        if (aboutEquals(cube.getLocationVector().x, randomXPos)) {
          list.add(cube);
        }
      }
      Cube[] layer = new Cube[list.size()];
      return list.toArray(layer);
    }

    private Cube[] getRandomYLayer() {
      List<Cube> list = new ArrayList<Cube>(size * size);
      float randomYPos = cubes[0].getReferenceVec().y + random.nextInt(size);

      for (Cube cube : cubes) {
        if (aboutEquals(cube.getLocationVector().y, randomYPos)) {
          list.add(cube);
        }
      }
      Cube[] layer = new Cube[list.size()];
      return list.toArray(layer);
    }

    private Cube[] getRandomZLayer() {
      List<Cube> list = new ArrayList<Cube>(size * size);
      float randomZPos = cubes[0].getReferenceVec().z + random.nextInt(size);

      for (Cube cube : cubes) {
        if (aboutEquals(cube.getLocationVector().z, randomZPos)) {
          list.add(cube);
        }
      }
      Cube[] layer = new Cube[list.size()];
      return list.toArray(layer);
    }

    private void quickRotate(Cube[] layer, Vector3d axis, double degrees) {
      for (Cube cube : layer) {
        cube.rotate(Quaternion3d.fromAxisAndAngle(axis, (float) Math.toRadians(degrees)));
      }
    }    
  }

}
